#region Copyright & License Information
/*
 * Copyright (c) The OpenRA Developers and Contributors
 * This file is part of OpenRA, which is free software. It is made
 * available to you under the terms of the GNU General Public License
 * as published by the Free Software Foundation, either version 3 of
 * the License, or (at your option) any later version. For more
 * information, see COPYING.
 */
#endregion

using System;
using System.Collections.Generic;
using System.IO;
using OpenRA.Primitives;

namespace OpenRA.Mods.Common.FileFormats
{
	public static class WavReader
	{
		enum WaveType : short { Pcm = 0x1, MsAdpcm = 0x2, ImaAdpcm = 0x11 }

		public static bool LoadSound(Stream s, out Func<Stream> result, out short channels, out int sampleBits, out int sampleRate, out float lengthInSeconds)
		{
			result = null;
			channels = -1;
			sampleBits = -1;
			sampleRate = -1;
			lengthInSeconds = -1;

			var type = s.ReadASCII(4);
			if (type != "RIFF")
				return false;

			s.ReadInt32(); // File-size
			var format = s.ReadASCII(4);
			if (format != "WAVE")
				return false;

			WaveType audioType = 0;
			var dataOffset = -1L;
			var dataSize = -1;
			var uncompressedSize = -1;
			short blockAlign = -1;
			while (s.Position < s.Length)
			{
				if ((s.Position & 1) == 1)
					s.ReadUInt8(); // Alignment

				if (s.Position == s.Length)
					break; // Break if we aligned with end of stream

				var blockType = s.ReadASCII(4);
				var chunkSize = s.ReadUInt32();

				switch (blockType)
				{
					case "fmt ":
						var audioFormat = s.ReadInt16();
						audioType = (WaveType)audioFormat;

						if (!Enum.IsDefined(audioType))
							throw new NotSupportedException($"Compression type {audioFormat} is not supported.");

						channels = s.ReadInt16();
						sampleRate = s.ReadInt32();
						s.ReadInt32(); // Byte Rate
						blockAlign = s.ReadInt16();
						sampleBits = s.ReadInt16();
						lengthInSeconds = (float)(s.Length * 8) / (channels * sampleRate * sampleBits);
						s.Position += chunkSize - 16; // Ignoring any optional extra params
						break;
					case "fact":
						uncompressedSize = s.ReadInt32();
						s.Position += chunkSize - 4; // Ignoring other formats than ADPCM, fact chunk not in standard PCM files
						break;
					case "data":
						if (s.Position + chunkSize > s.Length)
							chunkSize = (uint)(s.Length - s.Position); // Handle defective data chunk size by assuming it's the remainder of the file

						dataOffset = s.Position;
						dataSize = (int)chunkSize;
						s.Position += chunkSize;
						break;
					case "LIST":
					case "cue ":
					default:
						s.Position += chunkSize; // Ignoring chunks we don't want to/know how to handle
						break;
				}
			}

			// sampleBits refers to the output bitrate, which is always 16 for adpcm.
			if (audioType != WaveType.Pcm)
				sampleBits = 16;

			var chan = channels;
			result = () =>
			{
				var audioStream = SegmentStream.CreateWithoutOwningStream(s, dataOffset, dataSize);
				if (audioType == WaveType.ImaAdpcm)
					return new WavStreamImaAdpcm(audioStream, dataSize, blockAlign, chan, uncompressedSize);
				if (audioType == WaveType.MsAdpcm)
					return new WavStreamMsAdpcm(audioStream, dataSize, blockAlign, chan);

				return audioStream; // Data is already PCM format.
			};

			return true;
		}

		sealed class WavStreamImaAdpcm : ReadOnlyAdapterStream
		{
			readonly short channels;
			readonly int numBlocks;
			readonly int blockDataSize;
			readonly int outputSize;
			readonly int[] predictor;
			readonly int[] index;

			readonly byte[] interleaveBuffer;
			int outOffset;
			int currentBlock;

			public WavStreamImaAdpcm(Stream stream, int dataSize, short blockAlign, short channels, int uncompressedSize)
				: base(stream)
			{
				this.channels = channels;
				numBlocks = dataSize / blockAlign;
				blockDataSize = blockAlign - channels * 4;
				outputSize = uncompressedSize * channels * 2;
				predictor = new int[channels];
				index = new int[channels];

				interleaveBuffer = new byte[channels * 16];
			}

			protected override bool BufferData(Stream baseStream, Queue<byte> data)
			{
				// Decode each block of IMA ADPCM data
				// Each block starts with a initial state per-channel
				for (var c = 0; c < channels; c++)
				{
					predictor[c] = baseStream.ReadInt16();
					index[c] = baseStream.ReadUInt8();
					baseStream.ReadUInt8(); // Unknown/Reserved

					// Output first sample from input
					data.Enqueue((byte)predictor[c]);
					data.Enqueue((byte)(predictor[c] >> 8));
					outOffset += 2;

					if (outOffset >= outputSize)
						return true;
				}

				// Decode and output remaining data in this block
				var blockOffset = 0;
				Span<byte> chunk = stackalloc byte[4];
				while (blockOffset < blockDataSize)
				{
					for (var c = 0; c < channels; c++)
					{
						// Decode 4 bytes (to 16 bytes of output) per channel
						baseStream.ReadBytes(chunk);
						var decoded = ImaAdpcmReader.LoadImaAdpcmSound(chunk, ref index[c], ref predictor[c]);

						// Interleave output, one sample per channel
						var interleaveChannelOffset = 2 * c;
						for (var i = 0; i < decoded.Length; i += 2)
						{
							var interleaveSampleOffset = interleaveChannelOffset + i;
							interleaveBuffer[interleaveSampleOffset] = decoded[i];
							interleaveBuffer[interleaveSampleOffset + 1] = decoded[i + 1];
							interleaveChannelOffset += 2 * (channels - 1);
						}

						blockOffset += 4;
					}

					var outputRemaining = outputSize - outOffset;
					var toCopy = Math.Min(outputRemaining, interleaveBuffer.Length);
					for (var i = 0; i < toCopy; i++)
						data.Enqueue(interleaveBuffer[i]);

					outOffset += 16 * channels;

					if (outOffset >= outputSize)
						return true;
				}

				return ++currentBlock >= numBlocks;
			}
		}

		// Format docs https://wiki.multimedia.cx/index.php/Microsoft_ADPCM
		public sealed class WavStreamMsAdpcm : ReadOnlyAdapterStream
		{
			static readonly int[] AdaptationTable =
			[
				230, 230, 230, 230, 307, 409, 512, 614,
				768, 614, 512, 409, 307, 230, 230, 230
			];

			static readonly int[] AdaptCoeff1 = [256, 512, 0, 192, 240, 460, 392];

			static readonly int[] AdaptCoeff2 = [0, -256, 0, 64, 0, -208, -232];

			readonly short channels;
			readonly int blockDataSize;
			readonly int numBlocks;

			int currentBlock;

			public WavStreamMsAdpcm(Stream stream, int dataSize, short blockAlign, short channels)
				: base(stream)
			{
				this.channels = channels;
				blockDataSize = blockAlign - channels * 7;
				numBlocks = dataSize / blockAlign;
			}

			protected override bool BufferData(Stream baseStream, Queue<byte> data)
			{
				var bpred = new byte[channels];
				var chanIdelta = new short[channels];

				var s1 = new short[channels];
				var s2 = new short[channels];

				for (var c = 0; c < channels; c++)
					bpred[c] = baseStream.ReadUInt8();

				for (var c = 0; c < channels; c++)
					chanIdelta[c] = baseStream.ReadInt16();

				for (var c = 0; c < channels; c++)
					s1[c] = baseStream.ReadInt16();

				for (var c = 0; c < channels; c++)
					s2[c] = WriteSample(baseStream.ReadInt16(), data);

				for (var c = 0; c < channels; c++)
					WriteSample(s1[c], data);

				var channelNumber = channels > 1 ? 1 : 0;

				for (var blockindx = 0; blockindx < blockDataSize; blockindx++)
				{
					var bytecode = baseStream.ReadUInt8();

					// Decode the first nibble, this is always left channel
					WriteSample(DecodeNibble((short)((bytecode >> 4) & 0x0F), bpred[0], ref chanIdelta[0], ref s1[0], ref s2[0]), data);

					// Decode the second nibble, for stereo this will be the right channel
					WriteSample(
						DecodeNibble(
							(short)(bytecode & 0x0F),
							bpred[channelNumber],
							ref chanIdelta[channelNumber],
							ref s1[channelNumber],
							ref s2[channelNumber]),
						data);
				}

				return ++currentBlock >= numBlocks;
			}

			static short WriteSample(short t, Queue<byte> data)
			{
				data.Enqueue((byte)t);
				data.Enqueue((byte)(t >> 8));
				return t;
			}

			// This code contains elements from libsndfile
			static short DecodeNibble(short nibble, byte bpred, ref short idelta, ref short s1, ref short s2)
			{
				var predict = (s1 * AdaptCoeff1[bpred] + s2 * AdaptCoeff2[bpred]) >> 8;

				var twosCompliment = (nibble & 0x8) > 0
					? nibble - 0x10
					: nibble;

				s2 = s1;
				s1 = (short)(twosCompliment * idelta + predict).Clamp(-32768, 32767);

				// Compute next Adaptive Scale Factor (ASF), saturating to lower bound of 16
				idelta = (short)((AdaptationTable[nibble] * idelta) >> 8);
				if (idelta < 16)
					idelta = 16;

				return s1;
			}
		}
	}
}
